console.clear();

import * as THREE from 'https://cdn.skypack.dev/three@0.125.0';
import gsap from "https://cdn.skypack.dev/gsap@3.5.1";

const BackgroundGradient = (colorA, colorB) => {
    var mesh = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(2,2,1,1),
        new THREE.ShaderMaterial({
            uniforms: {
                uColorA: { value: new THREE.Color(colorA) },
                uColorB: { value: new THREE.Color(colorB) }
            },
            vertexShader: `
            varying vec2 vUv;
            void main(){
                vUv = uv;
                float depth = -1.; //or maybe 1. you can experiment
                gl_Position = vec4(position.xy, depth, 1.);
            }
          `,
            fragmentShader:
                `
            varying vec2 vUv;
            uniform vec3 uColorA;
            uniform vec3 uColorB;
            void main(){
                gl_FragColor = vec4(
                    mix( uColorA, uColorB, vec3(vUv.y)),
                    1.
                );
            }
          `
        })
    )

    mesh.material.depthWrite = false
    mesh.renderOrder = -99999
    return mesh
}

class Stage
{
    constructor(domCanvasElement, topColor, bottomColor)
    {
        this.canvas = domCanvasElement
        this.scene = new THREE.Scene()

        this.sizes = {
            width: 0,
            height: 0
        }

        /**
         * Background Gradient
         */

        this.background = BackgroundGradient(bottomColor, topColor)
        this.scene.add(this.background)

        /**
         * Camera
         */

        this.camera = new THREE.PerspectiveCamera(30, this.sizes.width / this.sizes.height, 0.1, 100)
        this.camera.position.set(0, 0, 6)
        this.scene.add(this.camera)

        /**
         * Camera Group
         */

        this.cameraGroup = new THREE.Group();

        this.scene.add(this.cameraGroup);

        /**
         * Renderer
         */
        const renderer = new THREE.WebGLRenderer({
            canvas: this.canvas,
            antialias: window.devicePixelRatio === 1 ? true : false
        })

        renderer.physicallyCorrectLights = true
        renderer.outputEncoding = THREE.sRGBEncoding
        renderer.toneMapping = THREE.ACESFilmicToneMapping
        renderer.toneMappingExposure = 1

        this.renderer = renderer

        window.addEventListener('resize', () => { this.onResize() })

        this.onResize()

    }

    onResize()
    {
        // Update sizes
        this.sizes.width = window.innerWidth
        this.sizes.height = window.innerHeight

        if(this.sizes.width < 800) this.camera.position.z = 10
        else this.camera.position.z = 6

        // Update camera
        this.camera.aspect = this.sizes.width / this.sizes.height
        this.camera.updateProjectionMatrix()

        // Update renderer
        this.renderer.setSize(this.sizes.width, this.sizes.height)
        this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    }

    add(item)
    {
        this.scene.add(item)
    }

    cameraAdd(item)
    {
        this.cameraGroup.add(item)
    }

    render(elapsedTime)
    {
        this.cameraGroup.position.copy(this.camera.position)
        this.cameraGroup.rotation.copy(this.camera.rotation)
        this.renderer.render(this.scene, this.camera)
    }
}

class Loader
{
    constructor (color = 'black', shadow = "white", size = 1)
    {
        this.mesh = new THREE.Mesh(
            new THREE.PlaneBufferGeometry(2,2,1,1),
            new THREE.ShaderMaterial({
                uniforms: {
                    uColor: { value: new THREE.Color(color) },
                    uColorShadow: { value: new THREE.Color(shadow) },
                    uProgress: { value: 0 },
                    uAlpha: { value: 1 },
                    uNoiseSize: { value: size }
                },
                vertexShader: `
        
                
                void main(){
             
                    gl_Position = vec4(position.xy, 0.5, 1.);
                }
                `,
                fragmentShader: `

                uniform vec3 uColor;
                uniform float uAlpha;
            
                void main () {
                    gl_FragColor = vec4(uColor, uAlpha);
                }
            `,
                transparent: true,
                depthTest: false
            })
        )
    }

    get progress()
    {
        return this.mesh.material.uniforms.uProgress.value;
    }

    set progress(newValue)
    {
        this.mesh.material.uniforms.uProgress.value = newValue;
    }

    get alpha()
    {
        return this.mesh.material.uniforms.uAlpha.value;
    }

    set alpha(newValue)
    {
        this.mesh.material.uniforms.uAlpha.value = newValue;
    }

    get noiseSize()
    {
        return this.mesh.material.uniforms.uNoiseSize.value;
    }

    set noiseSize(newValue)
    {
        this.mesh.material.uniforms.uNoiseSize.value = newValue;
    }
}

const ripVertexShader = `
	uniform float uTearAmount;
	uniform float uTearWidth;
	uniform float uTearXAngle;
	uniform float uTearYAngle;
	uniform float uTearZAngle;
	uniform float uTearXOffset;
	uniform float uXDirection;
	uniform float uRipSide;
	uniform float uRipSeed;

	varying vec2 vUv;
	varying float vAmount;

	mat4 rotationX( in float angle ) {
		return mat4(	1.0,		0,			0,			0,
						0, 	cos(angle),	-sin(angle),		0,
						0, 	sin(angle),	 cos(angle),		0,
						0, 			0,			  0, 		1);
	}

	mat4 rotationY( in float angle ) {
		return mat4(	cos(angle),		0,		sin(angle),	0,
								0,		1.0,			 0,	0,
						-sin(angle),	0,		cos(angle),	0,
								0, 		0,				0,	1);
	}

	mat4 rotationZ( in float angle ) {
		return mat4(	cos(angle),		-sin(angle),	0,	0,
						sin(angle),		cos(angle),		0,	0,
								0,				0,		1,	0,
								0,				0,		0,	1);
	}

	void main(){

		float ripAmount = 0.0;
		float yAmount = max(0.0, (uTearAmount - (1.0 - uv.y)));
		float zRotate = uTearZAngle * yAmount;
		float xRotate = uTearXAngle * yAmount;
		float yRotate = uTearYAngle * yAmount;
		vec3 rotation = vec3(xRotate* yAmount, yRotate* yAmount, zRotate* yAmount);


		float halfHeight = float(HEIGHT) * 0.5;
		float halfWidth = (float(WIDTH) - uTearWidth * 0.5) * 0.5;

		vec4 vertex = vec4(position.x + (halfWidth * uXDirection) - halfWidth, position.y + halfHeight, position.z, 1.0);

		vertex = vertex * rotationY(rotation.y ) * rotationX(rotation.x  ) * rotationZ(rotation.z  );
		vertex.x += uTearXOffset * yAmount + ripAmount + halfWidth ;
		vertex.y -= halfHeight;

		vec4 modelPosition = modelMatrix * vertex;
		vec4 viewPosition = viewMatrix * modelPosition;
		vec4 projectedPosition = projectionMatrix * viewPosition;

		gl_Position = projectedPosition;

		vUv = uv;
		vAmount = yAmount;
	}
`

const ripFragmentShader = `
	uniform sampler2D uMap;
	uniform sampler2D uRip;
	uniform sampler2D uBorder;

	uniform vec3 uShadeColor;
	uniform float uUvOffset;
	uniform float uRipSide;
	uniform float uTearXAngle;
	uniform float uShadeAmount;
	uniform float uTearWidth;
	uniform float uWhiteThreshold;
	uniform float uTearOffset;

	varying vec2 vUv;
	varying float vAmount;

	void main () {

		bool rightSide = uRipSide == 1.0;
		float ripAmount = -1.0;
		float width = float(WIDTH);
		float widthOverlap = (uTearWidth * 0.5) + width;

		bool frontSheet = uTearXAngle > 0.0;

		float xScale = widthOverlap / float(FULL_WIDTH);
		vec2 uvOffset = vec2(vUv.x * xScale + uUvOffset, vUv.y);
		vec4 textureColor = texture2D(uMap, uvOffset);
		vec4 borderColor = texture2D(uBorder, uvOffset);
		if(borderColor.r > 0.0) textureColor = vec4(vec3(0.95), 1.0);

		float ripRange = uTearWidth / widthOverlap;
		float ripStart = rightSide ? 0.0 : 1.0 - ripRange;

		float alpha = 1.0;

		float ripX = (vUv.x - ripStart) / ripRange;
		float ripY = vUv.y * 0.5 + (0.5 * uTearOffset);
		vec4 ripCut = texture2D(uRip, vec2(ripX, ripY));
		vec4 ripColor = texture2D(uRip, vec2(ripX * 0.9, ripY - 0.02));

		float whiteness = dot(vec4(1.0), ripCut) / 4.0;

		if (!rightSide && whiteness <= uWhiteThreshold)
		{
			whiteness = dot(vec4(1.0), ripColor) / 4.0;
			if(whiteness >= uWhiteThreshold) textureColor = ripColor;
			else alpha = 0.0;
		} 
		if (rightSide && whiteness >= uWhiteThreshold) alpha = 0.0;

		gl_FragColor = mix(vec4(textureColor.rgb, alpha), vec4(uShadeColor, alpha), vAmount * uShadeAmount);
	}
`

class Photo
{
    constructor(textures, destoryCallback)
    {
        this.destroyCallback = destoryCallback
        this.photoTexture = textures.photo;
        this.borderTexture = textures.border;
        this.ripTexture = textures.rip;
        this.interactive = false;

        this.group = new THREE.Group();
        this.group.rotation.z = (Math.random() * 2 - 1) * Math.PI
        this.group.position.y = 10

        setTimeout(() => {
            this.interactive = true
        }, 400)

        const introAnimation = gsap.timeline({
            delay: 0.3,
            defaults: {
                duration: 0.8,
                ease: 'power3.out'
            }
        })
        introAnimation.to(this.group.rotation, {z: 0}, 0)
        introAnimation.to(this.group.position, {y: 0}, 0)

        const width = 3;
        const tearWidth = 0.4;

        this.sheetSettings = {
            widthSegments: 30,
            heightSegments: 50,
            tearOffset: Math.random(),
            width: width,
            height: 2,
            tearAmount: 0,
            tearWidth: tearWidth,
            ripWhiteThreshold: 0.7,
            left: {
                uvOffset: 0,
                ripSide: 0,
                tearXAngle: -0.01,
                tearYAngle: -0.1,
                tearZAngle: 0.05,
                tearXOffset: 0,
                direction: -1,
                shadeColor: new THREE.Color('white'),
                shadeAmount: 0.2
            },
            right: {
                uvOffset: ((width - tearWidth) / width) * 0.5,
                ripSide: 1,
                tearXAngle: 0.2,
                tearYAngle: 0.1,
                tearZAngle: -0.1,
                tearXOffset: 0,
                direction: 1,
                shadeColor: new THREE.Color('black'),
                shadeAmount: 0.4
            }
        }

        this.sides = [
            {
                id: 'left',
                mesh: null,
                material: null
            },
            {
                id: 'right',
                mesh: null,
                material: null
            }
        ]

        this.sheetPlane = new THREE.PlaneBufferGeometry(this.sheetSettings.width / 2 + this.sheetSettings.tearWidth / 2, this.sheetSettings.height, this.sheetSettings.widthSegments, this.sheetSettings.heightSegments);

        this.sides.forEach(side =>
        {
            side.material = this.getRipMaterial(side.id)
            side.mesh = new THREE.Mesh( this.sheetPlane, side.material)

            if(this.sheetSettings[side.id].tearXAngle > 0)
            {
                side.mesh.position.z += 0.0001
            }
            this.group.add(side.mesh);
        })


    }

    getRipMaterial(side)
    {
        const material =  new THREE.ShaderMaterial({
            defines: {
                HEIGHT:             this.sheetSettings.height,
                WIDTH:              this.sheetSettings.width / 2,
                FULL_WIDTH:         this.sheetSettings.width,
                HEIGHT_SEGMENTS:    this.sheetSettings.heightSegments,
                WIDTH_SEGMENTS:     this.sheetSettings.widthSegments,
            },
            uniforms: {
                uMap :               { value: this.photoTexture },
                uRip :               { value: this.ripTexture },
                uBorder :            { value: this.borderTexture },
                uRipSide :           { value: this.sheetSettings[side].ripSide },
                uTearWidth :         { value: this.sheetSettings.tearWidth },
                uWhiteThreshold:     { value: this.sheetSettings.ripWhiteThreshold },
                uTearAmount:         { value: this.sheetSettings.tearAmount },
                uTearOffset:         { value: this.sheetSettings.tearOffset },
                uUvOffset:           { value: this.sheetSettings[side].uvOffset },
                uTearXAngle:         { value: this.sheetSettings[side].tearXAngle },
                uTearYAngle:         { value: this.sheetSettings[side].tearYAngle },
                uTearZAngle:         { value: this.sheetSettings[side].tearZAngle },
                uTearXOffset:        { value: this.sheetSettings[side].tearXOffset },
                uXDirection:         { value: this.sheetSettings[side].direction },
                uShadeColor:         { value: this.sheetSettings[side].shadeColor },
                uShadeAmount:        { value: this.sheetSettings[side].shadeAmount },
            },
            transparent: true,
            vertexShader: ripVertexShader,
            fragmentShader: ripFragmentShader
        })

        return material;
    }

    shouldCompleteRip()
    {
        return this.sheetSettings.tearAmount >= 1.5;
    }

    updateUniforms()
    {
        if(this.interactive && this.shouldCompleteRip())
        {
            this.remove();
        }
        else
        {
            if(this.sheetSettings.tearAmount === 0) this.sheetSettings.tearOffset = Math.random();
            this.sides.forEach(side =>
            {
                const uniforms = side.mesh.material.uniforms

                uniforms.uTearAmount.value =        this.sheetSettings.tearAmount
                uniforms.uTearOffset.value =        this.sheetSettings.tearOffset
                uniforms.uUvOffset.value =          this.sheetSettings[side.id].uvOffset;
                uniforms.uTearXAngle.value =        this.sheetSettings[side.id].tearXAngle;
                uniforms.uTearYAngle.value =        this.sheetSettings[side.id].tearYAngle;
                uniforms.uTearZAngle.value =        this.sheetSettings[side.id].tearZAngle;
                uniforms.uTearXOffset.value =       this.sheetSettings[side.id].tearXOffset;
                uniforms.uXDirection.value =        this.sheetSettings[side.id].direction;
                uniforms.uShadeColor.value =        this.sheetSettings[side.id].shadeColor;
                uniforms.uShadeAmount.value =       this.sheetSettings[side.id].shadeAmount;
                uniforms.uWhiteThreshold.value =    this.sheetSettings.ripWhiteThreshold;
            })
        }

    }

    completeRip()
    {
        if(this.sheetSettings.tearAmount >= 1.15) this.remove()
        else this.reset()
    }

    reset()
    {
        gsap.to(this.sheetSettings, {tearAmount: 0, onUpdate: () => this.updateUniforms()})
    }

    remove()
    {
        this.interactive = false
        this.destroyCallback()
        const removeAnimation = gsap.timeline({ defaults: {duration: 1, ease: 'power2.in'}, onComplete: () => this.destroyMe() });
        removeAnimation.to(this.sheetSettings, {tearAmount: 1.5 + Math.random() * 1.5, ease: 'power2.out', onUpdate: () => this.updateUniforms()})
        removeAnimation.to(this.group.position, {z: 1}, 0)

        this.sides.forEach(side =>
        {
            removeAnimation.to(side.mesh.position, {y: -3 + (Math.random() * -3), x: (2 + (Math.random() * 3)) * (this.sheetSettings[side.id].ripSide - 0.5) }, 0)
            removeAnimation.to(side.mesh.rotation, {z: (-2 + Math.random() * -3) * (this.sheetSettings[side.id].ripSide - 0.5) }, 0)
        })
    }

    destroyMe()
    {
        this.sheetPlane.dispose()
        this.sides.forEach(side =>
        {
            side.material.dispose()
            this.group.remove(side.mesh)
        })
    }
}

let interacted = false;

/**
 * Stage
 */

const canvas = document.querySelector('canvas.webgl')
const stage = new Stage(canvas, '#F1EBE4', '#D5C3AE')

/**
 * Loaders
 */

const loaderScreen = new Loader('black');
stage.add(loaderScreen.mesh);

const loadingManager = new THREE.LoadingManager(
    () =>
    {
        const tl = gsap.timeline();
        tl.to(loaderScreen, {progress: 1, alpha: 0, duration: .5, ease: 'power4.inOut'}, 0)

        init();
    }
)

const textureLoader = new THREE.TextureLoader(loadingManager)

/**
 * Lights
 */

let envLight = new THREE.AmbientLight({color: 'white', intensity: 6})
stage.add(envLight)

let pointLight = new THREE.PointLight({color: 'white', intensity: 20})
pointLight.position.z = -1;
stage.add(pointLight)

/**
 * Materials
 */

const images = [
    {
        texture: textureLoader.load('https://assets.codepen.io/557388/photo-4.jpg'),
        colors: [[192,208,220], [217,190,174]]
    },
    {
        texture: textureLoader.load('https://assets.codepen.io/557388/photo-3.jpg'),
        colors: [[168,163,150], [218,180,141]]
    }
]
let currentImage = -1;
const textureRip = textureLoader.load('https://assets.codepen.io/557388/rip.jpg')
const textureBorder = textureLoader.load('https://assets.codepen.io/557388/border.png')

/**
 * Paper
 */

const photos = [];
const mouseStart = new THREE.Vector2()
let mouseDown = false;
const extraImages = [
    { file: 'https://assets.codepen.io/557388/photo-1.jpg', colors: [[208,229,224], [209,209,220]] },
    { file: 'https://assets.codepen.io/557388/photo-2.jpg', colors: [[191,192,184], [217,200,170]] },
    { file: 'https://assets.codepen.io/557388/photo-6.jpg', colors: [[217,226,233], [216,220,203]] },
    { file: 'https://assets.codepen.io/557388/photo-7.jpg', colors: [[206,221,226], [219,213,222]] },
    { file: 'https://assets.codepen.io/557388/photo-5.jpg', colors: [[199,199,210], [218,203,195]] },
]
const postInitTextureLoader = new THREE.TextureLoader()

const getMousePos = (x, y) =>
{
    return {
        x: (x / stage.sizes.width) * 2 - 1,
        y: - (y / stage.sizes.height) * 2 + 1
    }
}

const down = (x, y) =>
{
    if(photos.length && photos[0].interactive)
    {
        interacted = true;
        hideHand()
        let pos = getMousePos(x, y);
        mouseStart.x = pos.x
        mouseStart.y = pos.y
        mouseDown = true;
    }
}

const move = (x, y) =>
{
    if(mouseDown && photos.length && photos[0].interactive)
    {
        let pos = getMousePos(x, y);
        let distanceY = mouseStart.y - pos.y

        photos[0].sheetSettings.tearAmount = Math.max(2 * distanceY, 0)
        photos[0].updateUniforms();
    }
}

const up = () =>
{
    if(mouseDown && photos.length && photos[0].interactive)
    {
        mouseDown = false;
        photos[0].completeRip();
    }
}

const loadExtraPhoto = () =>
{
    const nextImage = extraImages.shift()
    images.push({
        texture: postInitTextureLoader.load(nextImage.file),
        colors: nextImage.colors
    })
}

const addNewPhoto = () =>
{
    currentImage++;
    if(currentImage >= images.length) currentImage = 0

    if(images.length - currentImage < 2 && extraImages.length) loadExtraPhoto()

    mouseDown = false;

    const nextImage = images[currentImage]

    let photo = new Photo(
        {
            photo: nextImage.texture,
            rip: textureRip,
            border: textureBorder
        },
        () => addNewPhoto()
    );
    photos.unshift(photo);
    stage.add(photo.group);

    gsap.to(stage.background.material.uniforms.uColorB.value, {
        r: nextImage.colors[0][0] / 255,
        g: nextImage.colors[0][1] / 255,
        b: nextImage.colors[0][2] / 255,
        ease:'power4.inOut',
        duration: 1
    })
    gsap.to(stage.background.material.uniforms.uColorA.value, {
        r: nextImage.colors[1][0] / 255,
        g: nextImage.colors[1][1] / 255,
        b: nextImage.colors[1][2] / 255,
        ease:'power4.inOut',
        duration: 1
    })
}

const init = () =>
{
    addNewPhoto();

    window.addEventListener('mousedown', (event) => down(event.clientX, event.clientY))
    window.addEventListener('touchstart', (event) => down(event.touches[0].clientX, event.touches[0].clientY))
    window.addEventListener('mousemove', (event) => move(event.clientX, event.clientY))
    window.addEventListener('touchmove', (event) => move(event.touches[0].clientX, event.touches[0].clientY))
    window.addEventListener('mouseup', up)
    window.addEventListener('touchend', up)
}

/**
 * Tick
 */

const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    stage.render(elapsedTime)
    window.requestAnimationFrame(tick)
}

tick()

const hand = document.getElementById('hand');
const downDuration = 2;
const upDuration = 0.7;

const hintAnimation = gsap.timeline({repeat: -1, defaults: {duration: downDuration, ease: 'power4.inOut'}});
hintAnimation.fromTo(hand, {y: '-100%'}, {y: '100%'})
hintAnimation.to(hand, {y: '-100%', duration: upDuration, motionPath: [{rotate: '-10'}, {rotate: '0'}]}, downDuration)
hintAnimation.to(hand, {rotate: '-25', scale: 1.1, duration: upDuration * 0.5, ease: 'power4.in'}, downDuration)
hintAnimation.to(hand, {rotate: '0', scale: 1, duration: upDuration * 0.5, ease: 'power4.out'}, downDuration + upDuration * 0.5)

hintAnimation.pause();

const showHand = () =>
{
    if(!interacted)
    {
        hintAnimation.play();
        gsap.to(hand, {opacity: 1})
    }
}

const hideHand = () =>
{
    gsap.to(hand, {opacity: 0, onComplete: () => hintAnimation.pause()})
}

setTimeout(() => showHand(), 5000)